package com.xuecheng.media.service.jobhandler;

import com.j256.simplemagic.ContentType;
import com.xuecheng.base.constant.MediaProcessConstant;
import com.xuecheng.base.util.Mp4VideoUtil;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.IMediaProcessService;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author 勾新杰
 * @version 1.0
 * @description: 视频处理任务
 * @date 2025/2/12 16:44
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class VideoXxlJob {

    /**
     * ffmpeg安装目录
     */
    @Value("${video-process.ffmpeg-path}")
    private String ffmpegPath;

    /**
     * minio客户端
     */
    private final MinioClient minioClient;

    /**
     * 待处理任务
     */
    private final IMediaProcessService mediaProcessService;

    /**
     * 视频处理任务
     */
    @XxlJob("videoJobHandler")
    public void videoJobHandler() throws InterruptedException {
        // 1. 获取当前执行器接收到的分片参数，获取要处理的视频列表
        // 1.1 获取分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        // 1.2 获取当前执行器允许的处理线程数
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        List<MediaProcess> mediaProcessList = mediaProcessService.getMediaProcessList(shardIndex, shardTotal, availableProcessors);
        // 1.3 如果没有需要处理的视频，直接返回
        if (mediaProcessList == null || mediaProcessList.isEmpty()) return;
        // 2. 创建线程池，进行视频处理任务
        int size = mediaProcessList.size();
        // 2.1 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(size);
        // 2.2 创建计数器，用于等待所有线程执行完成
        CountDownLatch countDownLatch = new CountDownLatch(size);
        // 2.3 创建线程，进行视频处理
        mediaProcessList.forEach(mediaProcess -> {
            threadPool.execute(() -> {
                try {
                    // 2.3.1 线程争抢获取到视频处理任务的分布式锁，两个videoJobHandler执行器会来抢分布式锁，抢到锁的线程会执行视频处理任务，否则就不执行视频处理任务
                    Long id = mediaProcess.getId();
                    boolean start = mediaProcessService.startProcess(id);
                    if (!start) return;
                    // 2.3.2 下载minio的视频到本地临时文件
                    String bucket = mediaProcess.getBucket();
                    String sourceMinioFilePath = mediaProcess.getFilePath();
                    Path sourcePath = downloadFromMinio(bucket, sourceMinioFilePath);
                    if (sourcePath == null) {
                        Integer failCount = mediaProcessService.fail(id);
                        if (Objects.equals(failCount, MediaProcessConstant.MAX_FAIL_COUNT))
                            mediaProcessService.failProcess(id, "已是最大失败次数，最后一次失败原因：minio视频下载失败");
                        return;
                    }
                    // 2.3.3 创建本地临时文件存放转码后的视频
                    Path targetPath = createTempFile("target", ".mp4");
                    if (targetPath == null) {
                        Integer failCount = mediaProcessService.fail(id);
                        if (Objects.equals(failCount, MediaProcessConstant.MAX_FAIL_COUNT))
                            mediaProcessService.failProcess(id, "已是最大失败次数，最后一次失败原因：创建临时文件失败");
                        return;
                    }
                    // 2.3.4 执行ffmpeg命令，进行视频转码
                    File targetFile = targetPath.toFile();
                    String sourceFileAbsolutePath = sourcePath.toFile().getAbsolutePath();
                    Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpegPath, sourceFileAbsolutePath, targetFile.getName(), targetFile.getAbsolutePath());
                    mp4VideoUtil.generateMp4();
                    // 2.3.5 将转码后的视频上传到minio
                    String filePath = getFilePath(mediaProcess.getFileId());
                    boolean upload = uploadToMinio(sourceFileAbsolutePath, bucket, filePath);
                    // 2.3.6 删除minio的源文件
                    boolean remove = removeFromMinio(bucket, sourceMinioFilePath);
                    if (!remove) {
                        Integer failCount = mediaProcessService.fail(id);
                        if (Objects.equals(failCount, MediaProcessConstant.MAX_FAIL_COUNT))
                            mediaProcessService.failProcess(id, "已是最大失败次数，最后一次失败原因：删除minio源文件失败");
                        return;
                    }
                    // 2.3.7 标记视频处理成功
                    if (upload) mediaProcessService.successProcess(id, getUrl(bucket, filePath));
                    else {
                        Integer failCount = mediaProcessService.fail(id);
                        if (Objects.equals(failCount, MediaProcessConstant.MAX_FAIL_COUNT))
                            mediaProcessService.failProcess(id, "已是最大失败次数，最后一次失败原因：上传文件失败");
                    }
                } finally {
                    countDownLatch.countDown();
                }
            });
        });
        boolean await = countDownLatch.await(MediaProcessConstant.MAX_PROCESS_TIME, TimeUnit.MINUTES);
        if (!await) log.error("视频处理任务超时");
    }

    /**
     * 从minio下载视频到本地
     *
     * @param bucket              桶名称
     * @param sourceMinioFilePath 文件路径
     * @return 下载后的文件路径
     */
    private boolean removeFromMinio(String bucket, String sourceMinioFilePath) {
        try {
            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(bucket)
                            .object(sourceMinioFilePath)
                            .build());
            return true;
        } catch (Exception e) {
            log.error("删除minio源文件失败:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取url
     *
     * @param bucket   桶名称
     * @param filePath 文件路径
     * @return url
     */
    private String getUrl(String bucket, String filePath) {
        return "/" + bucket + "/" + filePath;
    }

    /**
     * 创建临时文件
     *
     * @param prefix 前缀
     * @param suffix 后缀
     * @return 临时文件路径
     */
    private Path createTempFile(String prefix, String suffix) {
        try {
            return Files.createTempFile(prefix, suffix);
        } catch (IOException e) {
            log.error("创建临时文件失败:{}", e.getMessage());
            return null;
        }
    }

    /**
     * 获取文件路径
     *
     * @param fileMd5 文件md5
     * @return 文件路径
     */
    private String getFilePath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + fileMd5 + ".mp4";
    }

    /**
     * 下载文件到本地(临时)
     *
     * @param bucket   桶名称
     * @param filePath 文件路径
     * @return 本地文件路径
     */
    private Path downloadFromMinio(String bucket, String filePath) {
        try {
            // 1. 获取文件
            FilterInputStream response = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket)
                            .object(filePath)
                            .build());
            // 2. 创建临时文件
            Path tempFile = createTempFile("source", ".temp");
            if (tempFile == null) return null;
            // 3. 将文件写入临时文件，ffmpeg转码时不需要文件的后缀名合法，主要本质是视频文件即可
            Files.copy(response, tempFile, StandardCopyOption.REPLACE_EXISTING);
            response.close();
            return tempFile;
        } catch (Exception e) {
            log.error("从minio下载临时文件失败:{}", e.getMessage());
            return null;
        }
    }

    /**
     * 上传文件到minio
     *
     * @param path     文件路径
     * @param bucket   桶名称
     * @param filePath 文件路径
     * @return 是否上传成功
     */
    private boolean uploadToMinio(String path, String bucket, String filePath) {
        try {
            minioClient.uploadObject(
                    UploadObjectArgs.builder()
                            .bucket(bucket)
                            .object(filePath)
                            .filename(path)
                            .contentType(ContentType.MP4A.getMimeType())
                            .build());
            return true;
        } catch (Exception e) {
            log.error("上传文件到minio失败:{}", e.getMessage());
            return false;
        }
    }
}
